package org.apache.solr.schema;

import java.io.IOException;
import java.io.InputStream;
import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.lucene.analysis.util.ResourceLoader;
import org.apache.lucene.analysis.util.ResourceLoaderAware;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.response.XMLWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SolrConstantScoreQuery;
import org.apache.solr.search.function.ValueSourceRangeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Field type for support of monetary values.
 * <p>
 * See <a href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org/solr/CurrencyField</a>
 */
public class CurrencyField extends FieldType implements SchemaAware, ResourceLoaderAware {

    public static Logger log = LoggerFactory.getLogger(CurrencyField.class);

    protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency";
    protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass";
    protected static final Object PARAM_PRECISION_STEP = "precisionStep";
    protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider";
    protected static final String DEFAULT_DEFAULT_CURRENCY = "USD";
    protected static final String DEFAULT_PRECISION_STEP = "0";
    protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw";
    protected static final String FIELD_SUFFIX_CURRENCY = "_currency";
    private IndexSchema schema;
    protected FieldType fieldTypeCurrency;
    protected FieldType fieldTypeAmountRaw;
    private String exchangeRateProviderClass;
    private String defaultCurrency;
    private ExchangeRateProvider provider;

    @Override
    protected void init(IndexSchema schema, Map<String, String> args) {

        super.init(schema, args);
        if (this.isMultiValued()) {
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "CurrencyField types can not be multiValued: " + this.typeName);
        }
        this.schema = schema;
        this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS);
        this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY);

        if (this.defaultCurrency == null) {
            this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY;
        }

        if (this.exchangeRateProviderClass == null) {
            this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS;
        }

        if (java.util.Currency.getInstance(this.defaultCurrency) == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + this.defaultCurrency);
        }

        String precisionStepString = args.get(PARAM_PRECISION_STEP);
        if (precisionStepString == null) {
            precisionStepString = DEFAULT_PRECISION_STEP;
        }

        // Initialize field type for amount
        fieldTypeAmountRaw = new TrieLongField();
        fieldTypeAmountRaw.setTypeName("amount_raw_type_tlong");
        Map<String, String> map = new HashMap<>(1);
        map.put("precisionStep", precisionStepString);
        fieldTypeAmountRaw.init(schema, map);

        // Initialize field type for currency string
        fieldTypeCurrency = new StrField();
        fieldTypeCurrency.setTypeName("currency_type_string");
        fieldTypeCurrency.init(schema, new HashMap<String, String>());

        args.remove(PARAM_RATE_PROVIDER_CLASS);
        args.remove(PARAM_DEFAULT_CURRENCY);
        args.remove(PARAM_PRECISION_STEP);

        try {
            Class<? extends ExchangeRateProvider> c = schema.getResourceLoader().findClass(exchangeRateProviderClass, ExchangeRateProvider.class);
            provider = c.newInstance();
            provider.init(args);
        }
        catch (InstantiationException | IllegalAccessException e) {
            throw new SolrException(ErrorCode.BAD_REQUEST, "Error instansiating exhange rate provider " + exchangeRateProviderClass + ". Please check your FieldType configuration", e);
        }
    }

    @Override
    public boolean isPolyField() {
        return true;
    }

    @Override
    public void checkSchemaField(final SchemaField field) throws SolrException {

        super.checkSchemaField(field);
        if (field.multiValued()) {
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
                    "CurrencyFields can not be multiValued: " + field.getName());
        }
    }

    @Override
    public IndexableField[] createFields(SchemaField field, Object externalVal, float boost) {

        CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);

        IndexableField[] f = new IndexableField[field.stored() ? 3 : 2];
        SchemaField amountField = getAmountField(field);
        f[0] = amountField.createField(String.valueOf(value.getAmount()), amountField.indexed() && !amountField.omitNorms() ? boost : 1F);
        SchemaField currencyField = getCurrencyField(field);
        f[1] = currencyField.createField(value.getCurrencyCode(), currencyField.indexed() && !currencyField.omitNorms() ? boost : 1F);

        if (field.stored()) {
            org.apache.lucene.document.FieldType customType = new org.apache.lucene.document.FieldType();
            assert !customType.omitNorms();
            customType.setStored(true);
            String storedValue = externalVal.toString().trim();
            if (storedValue.indexOf(",") < 0) {
                storedValue += "," + defaultCurrency;
            }
            f[2] = createField(field.getName(), storedValue, customType, 1F);
        }

        return f;
    }

    private SchemaField getAmountField(SchemaField field) {
        return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
    }

    private SchemaField getCurrencyField(SchemaField field) {
        return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY);
    }

    private void createDynamicCurrencyField(String suffix, FieldType type) {

        String name = "*" + POLY_FIELD_SEPARATOR + suffix;
        Map<String, String> props = new HashMap<>();
        props.put("indexed", "true");
        props.put("stored", "false");
        props.put("multiValued", "false");
        props.put("omitNorms", "true");
        int p = SchemaField.calcProps(name, type, props);
        schema.registerDynamicField(SchemaField.create(name, type, p, null));
    }

    /**
     * When index schema is informed, add dynamic fields.
     *
     * @param indexSchema The index schema.
     */
    @Override
    public void inform(IndexSchema indexSchema) {
        createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, fieldTypeCurrency);
        createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, fieldTypeAmountRaw);
    }

    /**
     * Load the currency config when resource loader initialized.
     *
     * @param resourceLoader The resource loader.
     */
    @Override
    public void inform(ResourceLoader resourceLoader) {
        provider.inform(resourceLoader);
        boolean reloaded = provider.reload();
        if (!reloaded) {
            log.warn("Failed reloading currencies");
        }
    }

    @Override
    public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {

        CurrencyValue value = CurrencyValue.parse(externalVal, defaultCurrency);
        CurrencyValue valueDefault;
        valueDefault = value.convertTo(provider, defaultCurrency);

        return getRangeQuery(parser, field, valueDefault, valueDefault, true, true);
    }

    @Override
    public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) {

        final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency);
        final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency);

        if (p1 != null && p2 != null && !p1.getCurrencyCode().equals(p2.getCurrencyCode())) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                    "Cannot parse range query " + part1 + " to " + part2
                    + ": range queries only supported when upper and lower bound have same currency.");
        }

        return getRangeQuery(parser, field, p1, p2, minInclusive, maxInclusive);
    }

    public Query getRangeQuery(QParser parser, SchemaField field, final CurrencyValue p1, final CurrencyValue p2, final boolean minInclusive, final boolean maxInclusive) {

        String currencyCode = (p1 != null) ? p1.getCurrencyCode() : (p2 != null) ? p2.getCurrencyCode() : defaultCurrency;
        final CurrencyValueSource vs = new CurrencyValueSource(field, currencyCode, parser);

        return new SolrConstantScoreQuery(new ValueSourceRangeFilter(vs,
                p1 == null ? null : p1.getAmount() + "", p2 == null ? null : p2.getAmount() + "", minInclusive, maxInclusive));
    }

    @Override
    public SortField getSortField(SchemaField field, boolean reverse) {
        try {
            // Convert all values to default currency for sorting.
            return (new CurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
        }
        catch (IOException e) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
        }
    }

    public void write(XMLWriter xmlWriter, String name, IndexableField field) throws IOException {
        xmlWriter.writeStr(name, field.stringValue(), false);
    }

    @Override
    public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException {
        writer.writeStr(name, field.stringValue(), false);
    }

    public ExchangeRateProvider getProvider() {
        return provider;
    }

    class CurrencyValueSource extends ValueSource {

        private static final long serialVersionUID = 1L;
        private String targetCurrencyCode;
        private ValueSource currencyValues;
        private ValueSource amountValues;
        private final SchemaField sf;

        public CurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
            this.sf = sfield;
            this.targetCurrencyCode = targetCurrencyCode;

            SchemaField amountField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
            SchemaField currencyField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY);

            currencyValues = currencyField.getType().getValueSource(currencyField, parser);
            amountValues = amountField.getType().getValueSource(amountField, parser);
        }

        @Override
        public FunctionValues getValues(Map context, AtomicReaderContext reader) throws IOException {
            final FunctionValues amounts = amountValues.getValues(context, reader);
            final FunctionValues currencies = currencyValues.getValues(context, reader);

            return new FunctionValues() {
                private final int MAX_CURRENCIES_TO_CACHE = 256;
                private final int[] fractionDigitCache = new int[MAX_CURRENCIES_TO_CACHE];
                private final String[] currencyOrdToCurrencyCache = new String[MAX_CURRENCIES_TO_CACHE];
                private final double[] exchangeRateCache = new double[MAX_CURRENCIES_TO_CACHE];
                private int targetFractionDigits = -1;
                private int targetCurrencyOrd = -1;
                private boolean initializedCache;

                private String getDocCurrencyCode(int doc, int currencyOrd) {
                    if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
                        String currency = currencyOrdToCurrencyCache[currencyOrd];

                        if (currency == null) {
                            currencyOrdToCurrencyCache[currencyOrd] = currency = currencies.strVal(doc);
                        }

                        if (currency == null) {
                            currency = defaultCurrency;
                        }

                        if (targetCurrencyOrd == -1 && currency.equals(targetCurrencyCode)) {
                            targetCurrencyOrd = currencyOrd;
                        }

                        return currency;
                    } else {
                        return currencies.strVal(doc);
                    }
                }

                @Override
                public long longVal(int doc) {
                    if (!initializedCache) {
                        for (int i = 0; i < fractionDigitCache.length; i++) {
                            fractionDigitCache[i] = -1;
                        }

                        initializedCache = true;
                    }

                    long amount = amounts.longVal(doc);
                    int currencyOrd = currencies.ordVal(doc);

                    if (currencyOrd == targetCurrencyOrd) {
                        return amount;
                    }

                    double exchangeRate;
                    int sourceFractionDigits;

                    if (targetFractionDigits == -1) {
                        targetFractionDigits = Currency.getInstance(targetCurrencyCode).getDefaultFractionDigits();
                    }

                    if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
                        exchangeRate = exchangeRateCache[currencyOrd];

                        if (exchangeRate <= 0.0) {
                            String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
                            exchangeRate = exchangeRateCache[currencyOrd] = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
                        }

                        sourceFractionDigits = fractionDigitCache[currencyOrd];

                        if (sourceFractionDigits == -1) {
                            String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
                            sourceFractionDigits = fractionDigitCache[currencyOrd] = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
                        }
                    }
                    else {
                        String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
                        exchangeRate = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
                        sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
                    }

                    return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits);
                }

                @Override
                public int intVal(int doc) {
                    return (int) longVal(doc);
                }

                @Override
                public double doubleVal(int doc) {
                    return (double) longVal(doc);
                }

                @Override
                public float floatVal(int doc) {
                    return (float) longVal(doc);
                }

                @Override
                public String strVal(int doc) {
                    return Long.toString(longVal(doc));
                }

                @Override
                public String toString(int doc) {
                    return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')';
                }
            };
        }

        public String name() {
            return "currency";
        }

        @Override
        public String description() {
            return name() + "(" + sf.getName() + ")";
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            CurrencyValueSource that = (CurrencyValueSource) o;

            return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null)
                    && !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null)
                    && !(targetCurrencyCode != null ? !targetCurrencyCode.equals(that.targetCurrencyCode) : that.targetCurrencyCode != null);

        }

        @Override
        public int hashCode() {
            int result = targetCurrencyCode != null ? targetCurrencyCode.hashCode() : 0;
            result = 31 * result + (currencyValues != null ? currencyValues.hashCode() : 0);
            result = 31 * result + (amountValues != null ? amountValues.hashCode() : 0);
            return result;
        }
    }
}

/**
 * Configuration for currency. Provides currency exchange rates.
 */
class FileExchangeRateProvider implements ExchangeRateProvider {

    public static Logger log = LoggerFactory.getLogger(FileExchangeRateProvider.class);
    protected static final String PARAM_CURRENCY_CONFIG = "currencyConfig";
    // Exchange rate map, maps Currency Code -> Currency Code -> Rate
    private Map<String, Map<String, Double>> rates = new HashMap<>();
    private String currencyConfigFile;
    private ResourceLoader loader;

    /**
     * Returns the currently known exchange rate between two currencies. If a
     * direct rate has been loaded, it is used. Otherwise, if a rate is known to
     * convert the target currency to the source, the inverse exchange rate is
     * computed.
     *
     * @param sourceCurrencyCode The source currency being converted from.
     * @param targetCurrencyCode The target currency being converted to.
     * @return The exchange rate.
     * @throws SolrException if the requested currency pair cannot be found
     */
    @Override
    public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) {

        if (sourceCurrencyCode == null || targetCurrencyCode == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null.");
        }

        if (sourceCurrencyCode.equals(targetCurrencyCode)) {
            return 1.0;
        }

        Double directRate = lookupRate(sourceCurrencyCode, targetCurrencyCode);

        if (directRate != null) {
            return directRate;
        }

        Double symmetricRate = lookupRate(targetCurrencyCode, sourceCurrencyCode);
        if (symmetricRate != null) {
            return 1.0 / symmetricRate;
        }

        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No available conversion rate between " + sourceCurrencyCode + " to " + targetCurrencyCode);
    }

    /**
     * Looks up the current known rate, if any, between the source and target
     * currencies.
     *
     * @param sourceCurrencyCode The source currency being converted from.
     * @param targetCurrencyCode The target currency being converted to.
     * @return The exchange rate, or null if no rate has been registered.
     */
    private Double lookupRate(String sourceCurrencyCode, String targetCurrencyCode) {

        Map<String, Double> rhs = rates.get(sourceCurrencyCode);
        if (rhs != null) {
            return rhs.get(targetCurrencyCode);
        }

        return null;
    }

    /**
     * Registers the specified exchange rate.
     *
     * @param ratesMap The map to add rate to
     * @param sourceCurrencyCode The source currency.
     * @param targetCurrencyCode The target currency.
     * @param rate The known exchange rate.
     */
    private void addRate(Map<String, Map<String, Double>> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) {

        Map<String, Double> rhs = ratesMap.get(sourceCurrencyCode);
        if (rhs == null) {
            rhs = new HashMap<>();
            ratesMap.put(sourceCurrencyCode, rhs);
        }

        rhs.put(targetCurrencyCode, rate);
    }

    @Override
    public boolean equals(Object o) {

        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        FileExchangeRateProvider that = (FileExchangeRateProvider) o;

        return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
    }

    @Override
    public int hashCode() {
        return rates != null ? rates.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "[" + this.getClass().getName() + " : " + rates.size() + " rates.]";
    }

    @Override
    public Set<String> listAvailableCurrencies() {

        Set<String> currencies = new HashSet<>();
        for (String from : rates.keySet()) {
            currencies.add(from);
            for (String to : rates.get(from).keySet()) {
                currencies.add(to);
            }
        }
        return currencies;
    }

    @Override
    public boolean reload() throws SolrException {

        //InputStream is = null;
        Map<String, Map<String, Double>> tmpRates = new HashMap<>();
        try (InputStream is = loader.openResource(currencyConfigFile);) {
            log.info("Reloading exchange rates from file " + this.currencyConfigFile);

            //is = loader.openResource(currencyConfigFile);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            try {
                dbf.setXIncludeAware(true);
                dbf.setNamespaceAware(true);
            }
            catch (UnsupportedOperationException e) {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "XML parser doesn't support XInclude option", e);
            }

            try {
                Document doc = dbf.newDocumentBuilder().parse(is);
                XPathFactory xpathFactory = XPathFactory.newInstance();
                XPath xpath = xpathFactory.newXPath();

                // Parse exchange rates.
                NodeList nodes = (NodeList) xpath.evaluate("/currencyConfig/rates/rate", doc, XPathConstants.NODESET);

                for (int i = 0; i < nodes.getLength(); i++) {
                    Node rateNode = nodes.item(i);
                    NamedNodeMap attributes = rateNode.getAttributes();
                    Node from = attributes.getNamedItem("from");
                    Node to = attributes.getNamedItem("to");
                    Node rate = attributes.getNamedItem("rate");

                    if (from == null || to == null || rate == null) {
                        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Exchange rate missing attributes (required: from, to, rate) " + rateNode);
                    }

                    String fromCurrency = from.getNodeValue();
                    String toCurrency = to.getNodeValue();
                    Double exchangeRate;

                    if (java.util.Currency.getInstance(fromCurrency) == null
                            || java.util.Currency.getInstance(toCurrency) == null) {
                        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not find from currency specified in exchange rate: " + rateNode);
                    }

                    try {
                        exchangeRate = Double.parseDouble(rate.getNodeValue());
                    }
                    catch (NumberFormatException e) {
                        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not parse exchange rate: " + rateNode, e);
                    }

                    addRate(tmpRates, fromCurrency, toCurrency, exchangeRate);
                }
            }
            catch (    SAXException | IOException | ParserConfigurationException | XPathExpressionException e) {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e);
            }
        }
        catch (IOException e) {
            throw new SolrException(ErrorCode.BAD_REQUEST, "Error while opening Currency configuration file " + currencyConfigFile, e);
        }

        // Atomically swap in the new rates map, if it loaded successfully
        this.rates = tmpRates;
        return true;
    }

    @Override
    public void init(Map<String, String> params) throws SolrException {

        this.currencyConfigFile = params.get(PARAM_CURRENCY_CONFIG);
        if (currencyConfigFile == null) {
            throw new SolrException(ErrorCode.NOT_FOUND, "Missing required configuration " + PARAM_CURRENCY_CONFIG);
        }

        // Removing config params custom to us
        params.remove(PARAM_CURRENCY_CONFIG);
    }

    @Override
    public void inform(ResourceLoader loader) throws SolrException {

        if (loader == null) {
            throw new SolrException(ErrorCode.BAD_REQUEST, "Needs ResourceLoader in order to load config file");
        }
        this.loader = loader;
        reload();
    }
}

/**
 * Represents a Currency field value, which includes a long amount and ISO
 * currency code.
 */
class CurrencyValue {

    private long amount;
    private String currencyCode;

    /**
     * Constructs a new currency value.
     *
     * @param amount The amount.
     * @param currencyCode The currency code.
     */
    public CurrencyValue(long amount, String currencyCode) {
        this.amount = amount;
        this.currencyCode = currencyCode;
    }

    /**
     * Constructs a new currency value by parsing the specific input.
     * <p/>
     * Currency values are expected to be in the format
     * &lt;amount&gt;,&lt;currency code&gt;, for example, "500,USD" would
     * represent 5 U.S. Dollars.
     * <p/>
     * If no currency code is specified, the default is assumed.
     *
     * @param externalVal The value to parse.
     * @param defaultCurrency The default currency.
     * @return The parsed CurrencyValue.
     */
    public static CurrencyValue parse(String externalVal, String defaultCurrency) {

        if (externalVal == null) {
            return null;
        }
        String amount = externalVal;
        String code = defaultCurrency;

        if (externalVal.contains(",")) {
            String[] amountAndCode = externalVal.split(",");
            amount = amountAndCode[0];
            code = amountAndCode[1];
        }

        if (amount.equals("*")) {
            return null;
        }

        Currency currency = java.util.Currency.getInstance(code);
        if (currency == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + code);
        }

        try {
            double value = Double.parseDouble(amount);
            long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));

            return new CurrencyValue(currencyValue, code);
        }
        catch (NumberFormatException e) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
        }
    }

    /**
     * The amount of the CurrencyValue.
     *
     * @return The amount.
     */
    public long getAmount() {
        return amount;
    }

    /**
     * The ISO currency code of the CurrencyValue.
     *
     * @return The currency code.
     */
    public String getCurrencyCode() {
        return currencyCode;
    }

    /**
     * Performs a currency conversion & unit conversion.
     *
     * @param exchangeRates Exchange rates to apply.
     * @param sourceCurrencyCode The source currency code.
     * @param sourceAmount The source amount.
     * @param targetCurrencyCode The target currency code.
     * @return The converted indexable units after the exchange rate and
     * currency fraction digits are applied.
     */
    public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {

        double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
        return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
    }

    /**
     * Performs a currency conversion & unit conversion.
     *
     * @param exchangeRate Exchange rate to apply.
     * @param sourceFractionDigits The fraction digits of the source.
     * @param sourceAmount The source amount.
     * @param targetFractionDigits The fraction digits of the target.
     * @return The converted indexable units after the exchange rate and
     * currency fraction digits are applied.
     */
    public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {

        int digitDelta = targetFractionDigits - sourceFractionDigits;
        double value = ((double) sourceAmount * exchangeRate);

        if (digitDelta != 0) {
            if (digitDelta < 0) {
                for (int i = 0; i < -digitDelta; i++) {
                    value *= 0.1;
                }
            } else {
                for (int i = 0; i < digitDelta; i++) {
                    value *= 10.0;
                }
            }
        }

        return (long) value;
    }

    /**
     * Performs a currency conversion & unit conversion.
     *
     * @param exchangeRate Exchange rate to apply.
     * @param sourceCurrencyCode The source currency code.
     * @param sourceAmount The source amount.
     * @param targetCurrencyCode The target currency code.
     * @return The converted indexable units after the exchange rate and
     * currency fraction digits are applied.
     */
    public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {

        if (targetCurrencyCode.equals(sourceCurrencyCode)) {
            return sourceAmount;
        }

        int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
        Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
        int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
        return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
    }

    /**
     * Returns a new CurrencyValue that is the conversion of this CurrencyValue
     * to the specified currency.
     *
     * @param exchangeRates The exchange rate provider.
     * @param targetCurrencyCode The target currency code to convert this
     * CurrencyValue to.
     * @return The converted CurrencyValue.
     */
    public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
        return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
    }

    @Override
    public String toString() {
        return String.valueOf(amount) + "," + currencyCode;
    }
}
